#include "creditutility.h"
#include <functional>
#include <QCryptographicHash>
#include "Qaesencryption/qaesencryption.h"

QByteArray encrypt(const QByteArray& dec_data,bool studentKey) {
    QByteArray key=QCryptographicHash::hash((studentKey?keyStuStr:keyStr).toLocal8Bit(),QCryptographicHash::Sha256);
    QAESEncryption encry(QAESEncryption::AES_256, QAESEncryption::ECB, QAESEncryption::PKCS7);
    return encry.encode(dec_data, key);
}
QByteArray decrypt(const QByteArray& enc_data,bool studentKey) {
    QByteArray key=QCryptographicHash::hash((studentKey?keyStuStr:keyStr).toLocal8Bit(),QCryptographicHash::Sha256);
    QAESEncryption encry(QAESEncryption::AES_256, QAESEncryption::ECB, QAESEncryption::PKCS7);
    QByteArray dec_data = encry.decode(enc_data, key);
    return QAESEncryption::RemovePadding(dec_data, QAESEncryption::PKCS7);
}
QString sha256encode(const QString& data) {
    QByteArray byteArray = data.toUtf8();
    QByteArray hash = QCryptographicHash::hash(byteArray, QCryptographicHash::Sha256);
    QString strSHA256 = hash.toHex();
    return strSHA256;
}

/* A valid ID is not empty, and consists of only numbers */
bool isValidId(QString idStr) {
    int len=idStr.length();
    if(len<=0) return false;
    for(int ind=0;ind<len;ind++)
        if(!idStr[ind].isDigit()) return false;
    return true;
}

/* A valid password is not empty, and consists of only letters, numbers and characters _.@%# */
bool isValidPwd(QString pwdStr) {
    int len=pwdStr.length();
    if(len<=0) return false;
    for(int ind=0;ind<len;ind++)
        if(!(pwdStr[ind]=='_'||
              pwdStr[ind]=='.'||
              pwdStr[ind]=='@'||
              pwdStr[ind]=='%'||
              pwdStr[ind]=='#'||
              pwdStr[ind].isLetterOrNumber())) return false;
    return true;
}

Student::Student(QString _id,QString _name,QString _pwd):id(_id),name(_name),pwd(_pwd) {}
QJsonObject Student::getObj(bool requirePassword, bool sha256) {
    QJsonObject stuObj;
    stuObj.insert("id",id);
    stuObj.insert("name",name);
    if(requirePassword && !pwd.isEmpty()) {
        if(sha256) stuObj.insert("password",sha256encode(pwd));
        else stuObj.insert("password",pwd);
    }
    return stuObj;
}
bool Student::parseObj(const QJsonObject& stuObj) {
    id=stuObj["id"].toString();
    name=stuObj["name"].toString();
    pwd=stuObj["password"].toString();
    return isValidId(id)&&stuObj["id"].isString()&&stuObj["name"].isString();
}

QJsonArray Student::getArr(const QList<Student>& stuList, bool requirePassword, bool sha256) {
    QJsonArray stuArray;
    foreach(Student student,stuList)
        stuArray.append(student.getObj(requirePassword, sha256));
    return stuArray;
}
QList<Student> Student::getList(const QJsonArray& stuArray) {
    QList<Student> stuList;
    for(const QJsonValue& stuVal:stuArray) {
        if(!stuVal.isObject()) continue;
        QJsonObject stuObj = stuVal.toObject();
        Student student;
        if(student.parseObj(stuObj))
            stuList.append(student);
    }
    return stuList;
}

/* Logic of merging activity: common records are overwritten */
void Activity::merge(const Activity& that) {
    type = that.type;
    foreach(QString recName,that.records.keys()) {
        Activity::ActivityRecord record = that.records[recName];
        foreach(QString id,record.keys()) {
            records[recName][id] = record[id];
        }
    }
}

Activity::Activity(QString _name,ActivityType _type):name(_name),type(_type) {}
QJsonObject Activity::getObj() {
    QJsonObject actObj;
    actObj.insert("name",name);
    actObj.insert("type",type & allType);

    QJsonArray recsArray;
    foreach(QString recName,records.keys()) {
        QJsonObject recObj;
        recObj.insert("name",recName);
        QJsonArray recArray;
        ActivityRecord recDat = records[recName];
        foreach(QString id,recDat.keys())
            if(isValidId(id) &&
                recDat[id].first>0)
        {
            CreditRecord recSingle = recDat[id];
            QJsonObject recsingleObj;
            recsingleObj.insert("id",id);
            recsingleObj.insert("credit",int(recSingle.first));
            recsingleObj.insert("type",recSingle.second & allType);
            recArray.append(recsingleObj);
        }
        recObj.insert("record",recArray);
        recsArray.append(recObj);
    }
    actObj.insert("records",recsArray);
    return actObj;
}
bool Activity::parseObj(const QJsonObject& actObj) {
    name = actObj["name"].toString();
    type = ActivityType(actObj["type"].toInt(0));
    if(!actObj["name"].isString()) return false;
    if(actObj["type"].toInt(32)==32 || actObj["type"].toInt(32)<0) return false;

    records.clear();
    QJsonArray recsArray = actObj["records"].toArray();
    for(QJsonValue recVal:recsArray) {
        QJsonObject recObj=recVal.toObject();
        if(!recObj["name"].isString()) continue;
        QString recname = recObj["name"].toString();
        QJsonArray recArray = recObj["record"].toArray();
        for(QJsonValue recsingleVal:recArray) {
            QJsonObject recsingleObj = recsingleVal.toObject();
            if(!isValidId(recsingleObj["id"].toString())) continue;
            if(recsingleObj["credit"].toInt()<=0) continue;
            if(recsingleObj["type"].toInt(32)==32 || recsingleObj["type"].toInt(32)<0) continue;
            QString recid = recsingleObj["id"].toString();
            unsigned credit = recsingleObj["credit"].toInt();
            ActivityType rectype = ActivityType(recsingleObj["type"].toInt());
            records[recname][recid]={credit, rectype};
        }
    }
    return true;
}

QJsonArray Activity::getArr(const QList<Activity>& actList) {
    QJsonArray actArray;
    foreach(Activity activity,actList)
        actArray.append(activity.getObj());
    return actArray;
}
QList<Activity> Activity::getList(const QJsonArray& actArray) {
    QList<Activity> actList;
    for(QJsonValue actVal:actArray) {
        if(!actVal.isObject()) continue;
        QJsonObject actObj = actVal.toObject();
        Activity activity;
        if(activity.parseObj(actObj))
            actList.append(activity);
    }
    return actList;
}

/* Get list of credit records for a student, according to ID */
QList<CreditRecord> getRecordList(const Student& student, const QList<Activity>& actList) {
    QList<CreditRecord> result{};
    foreach(Activity activity,actList)
        foreach(Activity::ActivityRecord actrecord,activity.records)
            if(actrecord.contains(student.id))
                result.append({actrecord[student.id].first,
                               ActivityType(actrecord[student.id].second & activity.type)});
    return result;
}

/* Get total credit of a given student, according to ID */
unsigned getTotalCredits(const Student& student, const QList<Activity>& actList) {
    unsigned result = 0;
    foreach(Activity activity,actList)
        foreach(Activity::ActivityRecord actrecord,activity.records)
            if(actrecord.contains(student.id))
                result+=actrecord[student.id].first;
    return result;
}

/* hash for sorting in a map */
unsigned categoryCreditStatus::hash() const {
    categoryCreditStatus truncated = *this;
    truncated.trunc();
    unsigned hashVal = truncated.pracVis;
    hashVal = hashVal*(categoryCredit + 1) + truncated.volAct;
    hashVal = hashVal*(categoryCredit + 1) + truncated.stuWork;
    hashVal = hashVal*(categoryCredit + 1) + truncated.perDev;
    hashVal = hashVal*(categoryCredit + 1) + truncated.comLife;
    return hashVal;
}

/* weight for sorting */
unsigned categoryCreditStatus::weight() const {
    categoryCreditStatus truncated = *this;
    truncated.trunc();
    unsigned weightVal = truncated.comLife
                        + truncated.perDev
                        + truncated.pracVis
                        + truncated.stuWork
                        + truncated.volAct;
    weightVal = weightVal*(categoryCredit + 1) + truncated.pracVis;
    weightVal = weightVal*(categoryCredit + 1) + truncated.volAct;
    weightVal = weightVal*(categoryCredit + 1) + truncated.stuWork;
    weightVal = weightVal*(categoryCredit + 1) + truncated.perDev;
    weightVal = weightVal*(categoryCredit + 1) + truncated.comLife;
    return weightVal;
}
void categoryCreditStatus::trunc() {
    if(stuWork>categoryCredit) stuWork=categoryCredit;
    if(perDev>categoryCredit) perDev=categoryCredit;
    if(pracVis>categoryCredit) pracVis=categoryCredit;
    if(volAct>categoryCredit) volAct=categoryCredit;
    if(comLife>categoryCredit) comLife=categoryCredit;
}
ActivityType categoryCreditStatus::qualification() const {
    ActivityType result = noneType;
    if(stuWork>=categoryCredit) result = ActivityType(result | studentWork);
    if(perDev>=categoryCredit) result = ActivityType(result | personalDevelopment);
    if(pracVis>=categoryCredit) result = ActivityType(result | practiceVisit);
    if(volAct>=categoryCredit) result = ActivityType(result | voluntaryActivity);
    if(comLife>=categoryCredit) result = ActivityType(result | communityLife);
    return result;
}

categoryCreditStatus checkTypeStatus(const Student& student, const QList<Activity>& actList) {
    QMap<unsigned,unsigned> vis; // hashval - minimum pos
    categoryCreditStatus optimalStatus = {0,0,0,0,0};
    QList<CreditRecord> record = getRecordList(student, actList);
    unsigned len=record.length();
    std::function<void(categoryCreditStatus,unsigned,QList<unsigned> hashVals)>
        DFS = [&](categoryCreditStatus curStat, unsigned pos,QList<unsigned> hashVals) -> void
    {
        hashVals.append(curStat.hash());
        if(pos>=len) {
            for(unsigned ind=0;ind<hashVals.size();ind++)
                vis[hashVals[ind]] = ind;
            if(curStat.weight() > optimalStatus.weight())
                optimalStatus = curStat;
            return;
        }
        if(vis.contains(curStat.hash()))
            if(vis[curStat.hash()]<=pos) return;

        CreditRecord curRecord = record[pos];
        if(curRecord.second & studentWork)
            DFS({curStat.stuWork + curRecord.first,
                 curStat.perDev,
                 curStat.pracVis,
                 curStat.volAct,
                 curStat.comLife},
                pos + 1,
                hashVals);
        if(curRecord.second & personalDevelopment)
            DFS({curStat.stuWork,
                 curStat.perDev + curRecord.first,
                 curStat.pracVis,
                 curStat.volAct,
                 curStat.comLife},
                pos + 1,
                hashVals);
        if(curRecord.second & practiceVisit)
            DFS({curStat.stuWork,
                 curStat.perDev,
                 curStat.pracVis + curRecord.first,
                 curStat.volAct,
                 curStat.comLife},
                pos + 1,
                hashVals);
        if(curRecord.second & voluntaryActivity)
            DFS({curStat.stuWork,
                 curStat.perDev,
                 curStat.pracVis,
                 curStat.volAct + curRecord.first,
                 curStat.comLife},
                pos + 1,
                hashVals);
        if(curRecord.second & communityLife)
            DFS({curStat.stuWork,
                 curStat.perDev,
                 curStat.pracVis,
                 curStat.volAct,
                 curStat.comLife + curRecord.first},
                pos + 1,
                hashVals);
    };
    DFS({0,0,0,0,0},0,{});
    return optimalStatus;
}
